'use strict';
const path = require('path')
const fs = require('fs')
const fse = require('fs-extra')
const inquirer = require('inquirer')
const semver = require('semver')
const glob = require('glob')
const ejs = require('ejs')
const Commander = require('@gsh-cli/commander')
const Package = require('@gsh-cli/package')
const log = require('@gsh-cli/log')
const { spinnerStart, sleep, suitSpawn } = require('@gsh-cli/utils')

const getTemplate = require('./getTemplate')

const TYPE_PROJECT = 'project'
const TYPE_COMPONENT = 'component'

const TEMPLATE_TYPE_NORMAL = 'normal';
const TEMPLATE_TYPE_CUSTOM = 'custom';

// 设置命令白名单
const WHITE_COMMAND = ['npm', 'cnpm'];

class InitCommander extends Commander {
  constructor(argv) {
    super(argv)
    this.projectInfo = null
    this.templateInfo = null
    this.template = []
    this.npmPkg = null
  }
  init() {
    this.projectName = this._argv[0] || ''
    this.force = this._cmd.force
  }

  async exec() {
    try {
      // 准备阶段
      const projectInfo = await this.prepare()
      this.projectInfo = projectInfo
      // 下载模板
      await this.downloadTemplate()
      // 安装模板
      await this.installTemplate()
    } catch(err) {
      log.error(err)
    }
  }

  /* 
  * @Description: 项目准备
  * @return: Object 项目信息对象
  */   
  async prepare() {
    // 0.判断模板是否为空
    const template = await getTemplate()
    if(template && template.length > 0) {
      this.template = template
    } else {
      log.error('项目模板为空')
      return
    }
    // 1.获取项目创建的目录
    // process.cwd()等同于path.resolve('.')，与__dirname的区别是前者获取的是node执行的目录，后者是js执行的目录
    const dir = process.cwd()
    // 2.目录下是否为空
    const isEmpty = this.isCwdEmpty(dir)
  
    // 3.是否强制覆盖
    if(!isEmpty) {
      let ifContinue = false
      if(!this.force) {
        ifContinue = (await inquirer
        .prompt([
          {
            type: 'confirm',
            name: 'ifContinue',
            message: '当前文件夹不为空，是否继续创建？',
            default: false
          }
        ])).ifContinue
      }
      if(ifContinue || this.force) {
        // 用户二次确认
        const { ifConfirmDelete } = await inquirer
        .prompt([
          {
            type: 'confirm',
            name: 'ifConfirmDelete',
            message: '是否确认清空当前目录下的文件？',
            default: false
          }
        ])
        if(ifConfirmDelete) {
          fse.emptyDirSync(dir)
        } else {
          return
        }
      }
    }
    // 4.获取项目信息
    return await this.getProjectInfo()
  }

  /* 
  * @Description: 获取项目信息
  * @return: Object 项目信息
  */   
  async getProjectInfo() {
    function isValidName(v) {
      return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v);
    }
    
    let projectInfo = {}
    // 1.用户选择创建的是项目还是组件
    const { type } = await inquirer.prompt([
      {
        type: 'list',
        name: 'type',
        message: '选择创建项目类型',
        default: TYPE_PROJECT,
        choices: [{
          name: '项目',
          value: TYPE_PROJECT
        }, {
          name: '组件',
          value: TYPE_COMPONENT
        }]
      }
    ])
    // 2.无论类型是项目还是组件，都需要输入项目名和版本号
    const title = TYPE_PROJECT == 'project' ? '项目' : '组件'
    // 输入项目名
    const projectNamePrompt = {
      type: 'input',
      name: 'projectName',
      message: `请输入${title}名`,
      default: '',
      validate: function(v) {
        const done = this.async();
        setTimeout(function() {
          // 1.首字符必须为英文字符
          // 2.尾字符必须为英文或数字，不能为字符
          // 3.字符仅允许"-_"
          if (!isValidName(v)) {
            done(`请输入合法的${title}名称`);
            return;
          }
          done(null, true);
        }, 0);
      },
    }
    // 输入版本号
    const versionPrompt = {
      type: 'input',
      name: 'version',
      default: '1.0.0',
      message:  `请输入${title}版本号`,
      validate: function(v) {
        const done = this.async();
        setTimeout(function() {
          if (!semver.valid(v)) {
            done(`请输入合法的${title}版本号`);
            return;
          }
          done(null, true);
        }, 0);
      }
    }
    let projectPrompts = []
    // 如果命令有传项目名参数
    if(!isValidName(this.projectName)) {
      projectPrompts.push(projectNamePrompt)
    } else {
      projectInfo.projectName = this.projectName
    }
    projectPrompts.push(versionPrompt, {
      type: 'list',
      name: 'projectTemplate',
      message: `请选择${title}模板`,
      choices: this.createTemplate(type)
    })
    if(type === TYPE_PROJECT) {
      const project = await inquirer.prompt(projectPrompts)
      projectInfo = {
        ...projectInfo,
        type,
        ...project
      }
    } else if(type === TYPE_COMPONENT) {
      // 添加组件描述
      const descriptionPrompt = {
        type: 'input',
        name: 'componentDescription',
        message: '请输入组件描述信息',
        default: '',
        // 判空
        validate: function(v) {
          const done = this.async();
          setTimeout(function() {
            if (!v) {
              done('请输入组件描述信息');
              return;
            }
            done(null, true);
          }, 0);
        }
      }
      projectPrompts.push(descriptionPrompt)
      const component = await inquirer.prompt(projectPrompts)
      projectInfo = {
        ...projectInfo,
        type,
        ...component
      }
    }
    // 规范化项目名
    projectInfo.className = this.formatProjectName(projectInfo.projectName)
    projectInfo.description = projectInfo.componentDescription
    return projectInfo
  }
  
  createTemplate(type) {
    return this.template
    .filter(item => item.tag.includes(type))
    .map(item => ({
      name: item.name,
      value: item.npmName
    }))
  }

/* 
 * @Description: 将projectName的驼峰命名规范为-
 * @param: projectName
 * @return: 规范化后的项目名
*/   
  formatProjectName(projectName) {
    return require("kebab-case")(projectName).replace(/^-/g, '')
  }

/* 
 * @Description: 下载模板
*/   
  async downloadTemplate() {
    const { projectTemplate } = this.projectInfo
    const templateInfo = this.template.find(item => item.npmName === projectTemplate)
    // 创建Package实例并做下载或者更新包处理
    const targetPath = path.resolve(process.env.CLI_HOME_PATH , 'template')
    const storeDir = path.resolve(process.env.CLI_HOME_PATH , 'template', 'node_modules')
    const templatePkg = new Package({
      targetPath,
      storeDir,
      packageName: templateInfo.npmName,
      packageVersion: templateInfo.version
    });
    this.templateInfo = templateInfo
    this.cacheFilePath = templatePkg.cacheFilePath
    let spinner = null
    try {
      if(await templatePkg.exists()) {
        spinner = spinnerStart('模板更新中...');
        await templatePkg.update()
        await sleep()
        log.success('模板更新成功！')
      } else {
        spinner = spinnerStart('模板下载中...');
        await templatePkg.install()
        await sleep()
        log.success('模板下载成功！')
      }
      // 下载或者安装成功后的实体包
      this.npmPkg = templatePkg
    } catch(err) {
      throw err
    } finally {
      spinner && spinner.stop(true)
    }
  }

/* 
 * @Description: 判断命令是否在白名单内
 * @param: 命令
 * @return: boolean
*/   
  checkWhiteCommand(cmd) {
    if(WHITE_COMMAND.includes(cmd)) return cmd
    return null
  }

/* 
 * @Description: ejs根据配置项并对已复制的模板进行遍历渲染需要的ejs数据
 * @param: 配置项
*/   
  async ejsRender(options) {
    const dir = process.cwd()
    return new Promise((resolve, reject) => {
      glob("**", {
        cwd: dir, // 代表当前目录下的所有文件
        ignore: options.ignore || '',
        nodir: true // 不要输出文件夹名
      }, (err, files) => {
        if(err) reject(err)
        if(files && files.length > 0) {
          Promise.all(files.map(file => {
            return new Promise((resolve1, reject1) => {
              const filePath = path.resolve(dir, file)
              ejs.renderFile(filePath, this.projectInfo, {}, (err, result) => {
                if(err) {
                  reject1(err)
                } else {
                  fse.writeFileSync(filePath, result)
                  resolve1()
                }
              })
            })
          })).then((res) => {
            resolve(res)
          }, (err) => {
            reject(err)
          })
        }
      })
    })
  }

/* 
 * @Description: 使用node多线程执行命令
 * @param: 命令
 * @param: 错误信息
*/   
async execCommand(command, errMsg) {
  if(this.templateInfo[command]) {
    const cmdArray = this.templateInfo[command].split(' ')
    const cmd = this.checkWhiteCommand(cmdArray[0])
    const args = cmdArray.slice(1)
    if(!cmd) throw '命令不存在！命令：' + cmdArray[0]
    return new Promise((resolve, reject) => {
      const child = suitSpawn(cmd, args, {
        cwd: process.cwd(),
        stdio: 'inherit'
      })
      child.on('error', (e) => {
        reject(errMsg)
      })
      child.on('exit', (e) => {
        if (e === 0) {
          resolve(e)
        } else {
          reject(errMsg)
        }
      })
    })
  }
}


/* 
 * @Description: 安装模板
*/   
  async installTemplate() {
    // 判断是常规模板还是自定义模板
    if(this.templateInfo) {
      if(this.templateInfo.type) {
        if(this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
          this.installNormalTemplate()
        } else if(this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
          this.installCustomTemplate()
        } else {
          throw new Error('无法识别模板类型')
        }
      } else {
        // 如果没有
        this.templateInfo.type = TEMPLATE_TYPE_NORMAL
      }
    } else {
      throw new Error('模板信息为空')
    }
  }

/* 
 * @Description: 安装常规模板
*/   
  async installNormalTemplate() {
    console.log('安装常规模板')
    const spinner = spinnerStart('正在安装常规模板')
    try {
      // 思路：要将安装后的缓存文件复制到当前目录中
      const currentDir = process.cwd()
      const copyDir = path.resolve(this.cacheFilePath, 'template') 
      //确保有目录，无则创建
      fse.ensureDirSync(copyDir)
      fse.ensureDirSync(currentDir)
      fse.copySync(copyDir, currentDir)
    } catch(err) {
      throw err
    } finally {
      spinner.stop(true)
      log.success('常规模板安装成功')
    }
    // 执行installCommand和startCommand
    try {
      const templateIgnore = this.templateInfo.ignore || []
      const options = { ignore: ['**/node_modules/**', ...templateIgnore] }
      await this.ejsRender(options)
      await this.execCommand('installCommand')
      await this.execCommand('startCommand')
    } catch(err) {
      throw new Error(err)
    }
  }

/* 
 * @Description: 安装自定义模板
*/   
  async installCustomTemplate() {
    // 思路：获取包的index.js入口并执行文件
    if(await this.npmPkg.exists()) {
      try {
        const root = await this.npmPkg.getRootFile()
        const templatePath = path.resolve(this.npmPkg.cacheFilePath, 'template');
        const options = {
          templateInfo: this.templateInfo,
          projectInfo: this.projectInfo,
          templatePath
        }
        const { projectDescription } = await inquirer.prompt([{
          type: "input",
          name: 'projectDescription',
          message: '请输入项目模板的介绍',
          validate: function(v) {
            const done = this.async();
            setTimeout(function() {
              if (!v) {
                done(`介绍不能为空`);
                return;
              }
              done(null, true);
            }, 0);
          }
        }])
        options.projectInfo.description = projectDescription
        let code = `require('${root}')(${JSON.stringify(options)})`
        suitSpawn('node', ['-e', code], {
          cwd: process.cwd(),
          stdio: 'inherit'
        } )
      } catch(err) {
        throw new Error(err)
      }
    } else {
      throw new Error('自定义模板入口文件不存在！');
    }
  }

/* 
 * @Description: 判断当前node进程对应的路径是否为空文件夹
 * @param: 当前node进程对应的路径
*/   
  isCwdEmpty(cwdPath) {
    let files = fs.readdirSync(cwdPath)
    // 文件类型筛选，挑选出非影响项目的文件，例如.开头的或者是node_modules
    files = files.filter(file => !file.startsWith('.') && file.indexOf('node_modules') === -1)
    return files && files.length < 0
  }
}

/* 
 * @Description: init实例方法
 * @param: init命令参数
 * @return: init指令的子实例
*/ 
function init(argv) {
  return new InitCommander(argv);
}


module.exports = init;

